Merge pull request #281 from dsander/pushbullet-agent

Added pushbullet agent, refactored JSONPath merging

Andrew Cantino 11 gadi atpakaļ
vecāks
revīzija
d1c2fcc4a1

+ 39 - 0
app/concerns/json_path_options_overwritable.rb

@@ -0,0 +1,39 @@
1
+module JsonPathOptionsOverwritable
2
+  extend ActiveSupport::Concern
3
+  # Using this concern allows providing optional `<attribute>_path` options hash
4
+  # attributes which will then (if not blank) be interpolated using the provided JSONPath.
5
+  #
6
+  # Example options Hash:
7
+  # {
8
+  #   name: 'Huginn',
9
+  #   name_path: '$.name',
10
+  #   title: 'Hello from Huginn'
11
+  #   title_path: ''
12
+  # }
13
+  # Example event payload:
14
+  # {
15
+  #   name: 'dynamic huginn'
16
+  # }
17
+  # calling agent.merge_json_path_options(event) returns the following hash:
18
+  # {
19
+  #   name: 'dynamic huginn'
20
+  #   title: 'Hello from Huginn'
21
+  # }
22
+
23
+  private
24
+  def merge_json_path_options(event)
25
+    options.select { |k, v| options_with_path.include? k}.tap do |merged_options|
26
+      options_with_path.each do |a|
27
+        merged_options[a] = select_option(event, a)
28
+      end
29
+    end
30
+  end
31
+
32
+  def select_option(event, a)
33
+    if options[a.to_s + '_path'].present?
34
+      Utils.value_at(event.payload, options[a.to_s + '_path'])
35
+    else
36
+      options[a]
37
+    end
38
+  end
39
+end

+ 15 - 0
app/concerns/working_helpers.rb

@@ -0,0 +1,15 @@
1
+module WorkingHelpers
2
+  extend ActiveSupport::Concern
3
+
4
+  def event_created_within?(days)
5
+    last_event_at && last_event_at > days.to_i.days.ago
6
+  end
7
+
8
+  def recent_error_logs?
9
+    last_event_at && last_error_log_at && last_error_log_at > (last_event_at - 2.minutes)
10
+  end
11
+
12
+  def received_event_without_error?
13
+    (last_receive_at.present? && last_error_log_at.blank?) || (last_receive_at.present? && last_error_log_at.present? && last_receive_at > last_error_log_at)
14
+  end
15
+end

+ 1 - 8
app/models/agent.rb

@@ -11,6 +11,7 @@ class Agent < ActiveRecord::Base
11 11
   include MarkdownClassAttributes
12 12
   include JSONSerializedField
13 13
   include RDBMSFunctions
14
+  include WorkingHelpers
14 15
 
15 16
   markdown_class_attributes :description, :event_description
16 17
 
@@ -83,14 +84,6 @@ class Agent < ActiveRecord::Base
83 84
     raise "Implement me in your subclass"
84 85
   end
85 86
 
86
-  def event_created_within?(days)
87
-    last_event_at && last_event_at > days.to_i.days.ago
88
-  end
89
-
90
-  def recent_error_logs?
91
-    last_event_at && last_error_log_at && last_error_log_at > (last_event_at - 2.minutes)
92
-  end
93
-
94 87
   def create_event(attrs)
95 88
     if can_create_events?
96 89
       events.create!({ 

+ 3 - 17
app/models/agents/hipchat_agent.rb

@@ -1,5 +1,7 @@
1 1
 module Agents
2 2
   class HipchatAgent < Agent
3
+    include JsonPathOptionsOverwritable
4
+
3 5
     cannot_be_scheduled!
4 6
     cannot_create_events!
5 7
 
@@ -47,30 +49,14 @@ module Agents
47 49
     def receive(incoming_events)
48 50
       client = HipChat::Client.new(options[:auth_token])
49 51
       incoming_events.each do |event|
50
-        mo = merge_options event
52
+        mo = merge_json_path_options event
51 53
         client[mo[:room_name]].send(mo[:username], mo[:message], :notify => mo[:notify].to_s == 'true' ? 1 : 0, :color => mo[:color])
52 54
       end
53 55
     end
54 56
 
55 57
     private
56
-    def select_option(event, a)
57
-      if options[a.to_s + '_path'].present?
58
-        Utils.value_at(event.payload, options[a.to_s + '_path'])
59
-      else
60
-        options[a]
61
-      end
62
-    end
63
-
64 58
     def options_with_path
65 59
       [:room_name, :username, :message, :notify, :color]
66 60
     end
67
-
68
-    def merge_options event
69
-      options.select { |k, v| options_with_path.include? k}.tap do |merged_options|
70
-        options_with_path.each do |a|
71
-          merged_options[a] = select_option(event, a)
72
-        end
73
-      end
74
-    end
75 61
   end
76 62
 end

+ 67 - 0
app/models/agents/pushbullet_agent.rb

@@ -0,0 +1,67 @@
1
+module Agents
2
+  class PushbulletAgent < Agent
3
+    include JsonPathOptionsOverwritable
4
+
5
+    cannot_be_scheduled!
6
+    cannot_create_events!
7
+
8
+    description <<-MD
9
+      The Pushbullet agent sends pushes to a pushbullet device
10
+
11
+      To authenticate you need to set the `api_key`, you can find yours at your account page:
12
+
13
+      `https://www.pushbullet.com/account`
14
+
15
+      Currently you need to get a the device identification manually:
16
+
17
+      `curl -u <your api key here>: https://api.pushbullet.com/api/devices`
18
+
19
+      Put one of the retured `iden` strings into the `device_id` field.
20
+
21
+      You can provide a `title` and a `body`.
22
+
23
+      If you want to specify `title` or `body` per event, you can provide a [JSONPath](http://goessner.net/articles/JsonPath/) for each of them.
24
+    MD
25
+
26
+    def default_options
27
+      {
28
+        'api_key' => '',
29
+        'device_id' => '',
30
+        'title' => "Hello from Huginn!",
31
+        'title_path' => '',
32
+        'body' => '',
33
+        'body_path' => '',
34
+      }
35
+    end
36
+
37
+    def validate_options
38
+      errors.add(:base, "you need to specify a pushbullet api_key") unless options['api_key'].present?
39
+      errors.add(:base, "you need to specify a device_id") if options['device_id'].blank?
40
+    end
41
+
42
+    def working?
43
+      received_event_without_error?
44
+    end
45
+
46
+    def receive(incoming_events)
47
+      incoming_events.each do |event|
48
+        response = HTTParty.post "https://api.pushbullet.com/api/pushes", query_options(event)
49
+        error(response.body) if response.body.include? 'error'
50
+      end
51
+    end
52
+
53
+    private
54
+    def query_options(event)
55
+      mo = merge_json_path_options event
56
+      basic_options.deep_merge(:body => {:title => mo[:title], :body => mo[:body]})
57
+    end
58
+
59
+    def basic_options
60
+      {:basic_auth => {:username =>options[:api_key], :password=>''}, :body => {:device_iden => options[:device_id], :type => 'note'}}
61
+    end
62
+
63
+    def options_with_path
64
+      [:title, :body]
65
+    end
66
+  end
67
+end

+ 3 - 26
spec/models/agent_spec.rb

@@ -1,6 +1,9 @@
1 1
 require 'spec_helper'
2
+require 'models/concerns/working_helpers'
2 3
 
3 4
 describe Agent do
5
+  it_behaves_like WorkingHelpers
6
+
4 7
   describe ".run_schedule" do
5 8
     before do
6 9
       Agents::WeatherAgent.count.should > 0
@@ -610,32 +613,6 @@ describe Agent do
610 613
     end
611 614
   end
612 615
 
613
-  describe "recent_error_logs?" do
614
-    it "returns true if last_error_log_at is near last_event_at" do
615
-      agent = Agent.new
616
-
617
-      agent.last_error_log_at = 10.minutes.ago
618
-      agent.last_event_at = 10.minutes.ago
619
-      agent.recent_error_logs?.should be_true
620
-
621
-      agent.last_error_log_at = 11.minutes.ago
622
-      agent.last_event_at = 10.minutes.ago
623
-      agent.recent_error_logs?.should be_true
624
-
625
-      agent.last_error_log_at = 5.minutes.ago
626
-      agent.last_event_at = 10.minutes.ago
627
-      agent.recent_error_logs?.should be_true
628
-
629
-      agent.last_error_log_at = 15.minutes.ago
630
-      agent.last_event_at = 10.minutes.ago
631
-      agent.recent_error_logs?.should be_false
632
-
633
-      agent.last_error_log_at = 2.days.ago
634
-      agent.last_event_at = 10.minutes.ago
635
-      agent.recent_error_logs?.should be_false
636
-    end
637
-  end
638
-
639 616
   describe "scopes" do
640 617
     describe "of_type" do
641 618
       it "should accept classes" do

+ 3 - 23
spec/models/agents/hipchat_agent_spec.rb

@@ -1,6 +1,9 @@
1 1
 require 'spec_helper'
2
+require 'models/concerns/json_path_options_overwritable'
2 3
 
3 4
 describe Agents::HipchatAgent do
5
+  it_behaves_like JsonPathOptionsOverwritable
6
+
4 7
   before(:each) do
5 8
     @valid_params = {
6 9
                       'auth_token' => 'token',
@@ -49,29 +52,6 @@ describe Agents::HipchatAgent do
49 52
 
50 53
   end
51 54
 
52
-  describe "helpers" do
53
-    describe "select_option" do
54
-      it "should use the room_name_path if specified" do
55
-        @checker.options['room_name_path'] = "$.room_name"
56
-        @checker.send(:select_option, @event, :room_name).should == "test room"
57
-      end
58
-
59
-      it "should use the normal option when the path option is blank" do
60
-        @checker.send(:select_option, @event, :room_name).should == "test"
61
-      end
62
-    end
63
-
64
-    it "should merge all options" do
65
-      @checker.send(:merge_options, @event).deep_symbolize_keys.should == {
66
-        :room_name => "test",
67
-        :username => "Huggin user",
68
-        :message => "Looks like its going to rain",
69
-        :notify => false,
70
-        :color => "yellow"
71
-      }
72
-    end
73
-  end
74
-
75 55
   describe "#receive" do
76 56
     it "send a message to the hipchat" do
77 57
       any_instance_of(HipChat::Room) do |obj|

+ 80 - 0
spec/models/agents/pushbullet_agent_spec.rb

@@ -0,0 +1,80 @@
1
+require 'spec_helper'
2
+require 'models/concerns/json_path_options_overwritable'
3
+
4
+describe Agents::PushbulletAgent do
5
+  it_behaves_like JsonPathOptionsOverwritable
6
+
7
+  before(:each) do
8
+    @valid_params = {
9
+                      'api_key' => 'token',
10
+                      'device_id' => '124',
11
+                      'body_path' => '$.body',
12
+                      'title' => 'hello from huginn'
13
+                    }
14
+
15
+    @checker = Agents::PushbulletAgent.new(:name => "somename", :options => @valid_params)
16
+    @checker.user = users(:jane)
17
+    @checker.save!
18
+
19
+    @event = Event.new
20
+    @event.agent = agents(:bob_weather_agent)
21
+    @event.payload = { :body => 'One two test' }
22
+    @event.save!
23
+  end
24
+
25
+  describe "validating" do
26
+    before do
27
+      @checker.should be_valid
28
+    end
29
+
30
+    it "should require the api_key" do
31
+      @checker.options['api_key'] = nil
32
+      @checker.should_not be_valid
33
+    end
34
+
35
+    it "should require the device_id" do
36
+      @checker.options['device_id'] = nil
37
+      @checker.should_not be_valid
38
+    end
39
+  end
40
+
41
+  describe "helpers" do
42
+    it "it should return the correct basic_options" do
43
+      @checker.send(:basic_options).should == {:basic_auth => {:username =>@checker.options[:api_key], :password=>''},
44
+                                               :body => {:device_iden => @checker.options[:device_id], :type => 'note'}}
45
+    end
46
+
47
+
48
+    it "should return the query_options" do
49
+      @checker.send(:query_options, @event).should == @checker.send(:basic_options).deep_merge({
50
+        :body => {:title => 'hello from huginn', :body => 'One two test'}
51
+      })
52
+    end
53
+  end
54
+
55
+  describe "#receive" do
56
+    it "send a message to the hipchat" do
57
+      stub_request(:post, "https://token:@api.pushbullet.com/api/pushes").
58
+        with(:body => "device_iden=124&type=note&title=hello%20from%20huginn&body=One%20two%20test").
59
+        to_return(:status => 200, :body => "ok", :headers => {})
60
+      dont_allow(@checker).error
61
+      @checker.receive([@event])
62
+    end
63
+
64
+    it "should log resquests which return an error" do
65
+      stub_request(:post, "https://token:@api.pushbullet.com/api/pushes").
66
+        with(:body => "device_iden=124&type=note&title=hello%20from%20huginn&body=One%20two%20test").
67
+        to_return(:status => 200, :body => "error", :headers => {})
68
+      mock(@checker).error("error")
69
+      @checker.receive([@event])
70
+    end
71
+  end
72
+
73
+  describe "#working?" do
74
+    it "should not be working until the first event was received" do
75
+      @checker.should_not be_working
76
+      @checker.last_receive_at = Time.now
77
+      @checker.should be_working
78
+    end
79
+  end
80
+end

+ 31 - 0
spec/models/concerns/json_path_options_overwritable.rb

@@ -0,0 +1,31 @@
1
+require 'spec_helper'
2
+
3
+shared_examples_for JsonPathOptionsOverwritable do
4
+  before(:each) do
5
+    @valid_params = described_class.new.default_options
6
+
7
+    @checker = described_class.new(:name => "somename", :options => @valid_params)
8
+    @checker.user = users(:jane)
9
+
10
+    @event = Event.new
11
+    @event.agent = agents(:bob_weather_agent)
12
+    @event.payload = { :room_name => 'test room', :message => 'Looks like its going to rain', username: "Huggin user"}
13
+    @event.save!
14
+  end
15
+
16
+  describe "select_option" do
17
+    it "should use the room_name_path if specified" do
18
+      @checker.options['room_name_path'] = "$.room_name"
19
+      @checker.send(:select_option, @event, :room_name).should == "test room"
20
+    end
21
+
22
+    it "should use the normal option when the path option is blank" do
23
+      @checker.options['room_name'] = 'test'
24
+      @checker.send(:select_option, @event, :room_name).should == "test"
25
+    end
26
+  end
27
+
28
+  it "should merge all options" do
29
+    @checker.send(:merge_json_path_options, @event).symbolize_keys.keys.should == @checker.send(:options_with_path)
30
+  end
31
+end

+ 53 - 0
spec/models/concerns/working_helpers.rb

@@ -0,0 +1,53 @@
1
+require 'spec_helper'
2
+
3
+shared_examples_for WorkingHelpers do
4
+  describe "recent_error_logs?" do
5
+    it "returns true if last_error_log_at is near last_event_at" do
6
+      agent = Agent.new
7
+
8
+      agent.last_error_log_at = 10.minutes.ago
9
+      agent.last_event_at = 10.minutes.ago
10
+      agent.recent_error_logs?.should be_true
11
+
12
+      agent.last_error_log_at = 11.minutes.ago
13
+      agent.last_event_at = 10.minutes.ago
14
+      agent.recent_error_logs?.should be_true
15
+
16
+      agent.last_error_log_at = 5.minutes.ago
17
+      agent.last_event_at = 10.minutes.ago
18
+      agent.recent_error_logs?.should be_true
19
+
20
+      agent.last_error_log_at = 15.minutes.ago
21
+      agent.last_event_at = 10.minutes.ago
22
+      agent.recent_error_logs?.should be_false
23
+
24
+      agent.last_error_log_at = 2.days.ago
25
+      agent.last_event_at = 10.minutes.ago
26
+      agent.recent_error_logs?.should be_false
27
+    end
28
+  end
29
+  describe "received_event_without_error?" do
30
+    before do
31
+      @agent = Agent.new
32
+    end
33
+
34
+    it "should return false until the first event was received" do
35
+      @agent.received_event_without_error?.should == false
36
+      @agent.last_receive_at = Time.now
37
+      @agent.received_event_without_error?.should == true
38
+    end
39
+
40
+    it "should return false when the last error occured after the last received event" do
41
+      @agent.last_receive_at = Time.now - 1.minute
42
+      @agent.last_error_log_at = Time.now
43
+      @agent.received_event_without_error?.should == false
44
+    end
45
+
46
+    it "should return true when the last received event occured after the last error" do
47
+      @agent.last_receive_at = Time.now
48
+      @agent.last_error_log_at = Time.now - 1.minute
49
+      @agent.received_event_without_error?.should == true
50
+    end
51
+  end
52
+
53
+end

+ 7 - 2
spec/spec_helper.rb

@@ -1,8 +1,13 @@
1 1
 # This file is copied to spec/ when you run 'rails generate rspec:install'
2 2
 ENV["RAILS_ENV"] ||= 'test'
3 3
 
4
-require 'coveralls'
5
-Coveralls.wear!('rails')
4
+if ENV['COVERAGE']
5
+  require 'simplecov'
6
+  SimpleCov.start 'rails'
7
+else
8
+  require 'coveralls'
9
+  Coveralls.wear!('rails')
10
+end
6 11
 
7 12
 require File.expand_path("../../config/environment", __FILE__)
8 13
 require 'rspec/rails'